package sweet.toolbox

import sweet.helper.{ PropsContainer, Logger }
import sweet.toolbox.command._

/** Main entry point of Sweet Toolbox. */
object Main extends ToolboxConfig {
  // Register all avialable command.
  addCommands(
    "list" -> new ListSummary(commands),
    "create-project" -> new CreateProject(props),
    "create-controller" -> new CreateController(props)
  )
  
  // Main usage info.
  val usage = """
    | Sweet Commands Toolbox. 
    |
    | To use it, invoke with one of avialable commands:
    |   toolbox <commandName> [commandArguments ...]
    | 
    | To see a list of available commads:
    |   toolbox list
    |
    | To see individual command help:
    |   toolbox <commandName> help
  """.stripMargin
  
  def main(args: Array[String]){
    if(args.length < 1)
      throw new IllegalArgumentException(usage)
      
    val cmdName = args(0)
    val cmdArgs = args.drop(1)
    
    if(cmdName == "help" || cmdName == "--help" || cmdName == "-h")
      println(usage)
    else
      commands.get(cmdName) match {
        case Some(cmd) => {
          if(cmdArgs.length == 1 && cmdArgs(0) == "help")
            println(cmd.usage)
          else
            cmd.exec(cmdArgs)
        }
        case None => throw new IllegalArgumentException("Command "+cmdName+" not found")
      }
  }
}

/** Print list of command names and it's summary line. */
class ListSummary(cmds: scala.collection.Map[String, Command]) extends Command{
  def summary = "List of all available commands with summary."
  def usage = "No argument is needed."
  def exec(args: Array[String]){
    println()
    for((name,cmd) <- cmds) printf("%-20s %s\n", name, cmd.summary)
  }
}

/** A command trait can be executed and has a summary line and block of usage text. */
trait Command {
  def summary: String
  def usage: String
  def exec(args: Array[String]): Unit
}

trait ToolboxCommand extends Command with Logger {
  import freemarker.template._
  import java.io._
  import sweet.mvc.fm._
  import sweet.helper._  
  
  import StringHelper.string2helper // implicit string enhancer
  import FileHelper.file2helper // implicit java File enhancer
  import Regex.pattern2regex // implicit java File enhancer

  def toolboxProps: scala.collection.Map[String, String]
  
  val toolboxProjectFilename = ".toolboxProject"
  
  val filenameREPrefix = "R_"
  val filenameRESuffix = "_R"
  val filenameREEscape = filenameREPrefix
    
  def createFreemarkerConfig(templateDir: File): Configuration = {
    val fmConfig = new ScalaFreemarkerConfiguration
    logger.debug("Loading template from directory " +templateDir.getAbsolutePath) 
    fmConfig.setDirectoryForTemplateLoading(templateDir)
    
    fmConfig
  }
  
  lazy val excludeDirRE = Regex(toolboxProps("project.excludeDirPattern"))
  def excludeDir(dir: File) = excludeDirRE.find(dir.getPathname)
  
  lazy val excludeFileRE = Regex(toolboxProps("project.excludeFilePattern"))
  def excludeFile(file: File) = excludeFileRE.find(file.getName)
    
  def withEachTemplate(templateDir: File, destDir: File)(func: (Template, String)=>Unit){
    val fmCfg = createFreemarkerConfig(templateDir)
    val templateDirPathname = templateDir.getPathname
    val templateDirPathnameLenPlusOne = templateDirPathname.length+1
    //For each file in templateDir, run it through FM engine and re-create it on destDir
    templateDir.eachDir{ dir =>
      if(!excludeDir(dir)){
        val files = dir.listSortedFiles
        var count = 0
        for( ftl <- files){
          count +=1
          
          if(ftl.isFile && !excludeFile(ftl)){        
            val ftlPathname = ftl.getPathname.substring(templateDirPathnameLenPlusOne)
            logger.debug("Reading template file "+ftlPathname)            
            val template = fmCfg.getTemplate(ftlPathname)
            func(template, ftlPathname)
          }
        }
        
        //If there is no file to processed, copy the empty directory.
        if(count ==0){
          val destDirName = dir.getPathname
          logger.debug("Creating directory " + destDirName)
          new File(destDir, destDirName).mkdirs()
        } 
      }
    }
  }
  
  def withTemplate(ftl: File)(func: (Template, String)=>Unit){
    val fmCfg = createFreemarkerConfig(ftl.getParentFile)
    val ftlPathname = ftl.getPathname
    logger.debug("Reading template file "+ftlPathname)            
    val template = fmCfg.getTemplate(ftl.getName)
    func(template, ftlPathname)
  }
  
  /** Return a filename based on orig ftl pathname. And if the name has replacable
   * variable, do a lookup and replace it with the returned value.
   */
  def createDestinationFile(ftlPathname: String, destDir: File)(lookup: (String)=>String): File = {
    logger.debug("Orig ftlPathname " + ftlPathname)
    val names = Regex.findGroupsByDelimeters(ftlPathname, filenameREPrefix, filenameRESuffix, filenameREEscape)
    logger.debug("Variable names found: " + names)
    var pathname = new StringHelper(ftlPathname)
    for(name <- names){
      val variableName = filenameREPrefix+name+filenameRESuffix
      val modelValue = lookup(name)
      logger.debug("Replacing "+variableName+" with "+modelValue)
      pathname = pathname.fastReplace(variableName, modelValue)        
    }
    
    val destFile = new File(destDir, pathname.toString)
    val parentDir = destFile.getParentFile
    if(!parentDir.exists){
      logger.debug("Make dir path "+parentDir)
      parentDir.mkdirs()
    }
    destFile    
  }
}

/** A container for Commands objects */
trait CommandContainer { 
  private val _commands = new scala.collection.mutable.HashMap[String, Command]
  def commands = _commands.readOnly

  def addCommands(tuples: (String, Command)*){
    for((k,v) <- tuples) _commands(k) = v  
  }
}

trait ToolboxConfig extends CommandContainer with PropsContainer{
  addSysProps("sweet.toolbox.home", "./")
  
  //load config properties file
  addSysProps("sweet.toolbox.config", "classpath:config.properties")
  addProps(resourceAsStream(props("sweet.toolbox.config")))
}

//=====  
object NamesHelper {
  import sweet.helper._   
  import StringHelper.string2helper // implicit string enhancer
  
  def packageName(s: String): String = {
    var res = s
    res = res.fastReplace("-", "") //remove all -      
    res = res.fastReplace("_", "") //remove all _  
    res = res.toLowerCase //low case all letters    
    res
  }
  
  def camelCaseName(s: String): String = {
    var res = new StringBuilder(s)
    var pos = -1
    
    res.replace(0, 1, res.substring(0,1).toUpperCase) // cap first letter.
    
    //remove all - and next char to upper case
    while({ pos = res.indexOf("-"); pos>=0 })
      res.replace(pos, pos+2, res.substring(pos+1, pos+2).toUpperCase)
    
    //remove all _ and next char to upper case
    while({ pos = res.indexOf("_"); pos>=0 })
      res.replace(pos, pos+2, res.substring(pos+1, pos+2).toUpperCase)
    
    res.toString
  }  
}
/** Provide reflection to Scala types. */
trait ScalaReflection extends Logger{
  
  import scala.collection.mutable
  import sweet.helper._
  
  type Getter = () => AnyRef
  type Setter = AnyRef => AnyRef 
  
  val unwantedMethodNames = Set[String]() ++
      (for(m <- classOf[Object].getMethods) yield m.getName) ++
      (for(m <- classOf[ScalaReflection].getMethods) yield m.getName)
  
  /** Scala getters are those public method that doesn't ends with '=$' and
   * with zero arguments. */
  lazy val getters = {
    val map = new mutable.HashMap[String, Getter]
    this.getClass.getDeclaredMethods.foreach { m =>
      val name = m.getName
      if(m.getParameterTypes.length==0 && !name.contains("$") && !unwantedMethodNames.contains(name)){
        map += Pair(name, (() => m.invoke(this, Array())))
      }
    }
    map.readOnly
  }
  /** Scala setters that are public method ends with '=$' and has exactly one parameter of
   * clz given. */
  lazy val setters = {
    val map = new mutable.HashMap[String, Setter]
    this.getClass.getDeclaredMethods.foreach { m =>
      val name = m.getName
      if(m.getParameterTypes.length==1 && name.endsWith("_$eq") && !unwantedMethodNames.contains(name)){
        //the key needs to remove the "_$eq" suffix!
        map += Pair(name.substring(0, name.length - 4), ((value: AnyRef) => m.invoke(this, Array(value))))
      }
    }
    //logger.debug(">>> setters" + map)
    map.readOnly
  }
}

import sweet.helper.Logger
trait FilePropsModel extends ScalaReflection with Logger {
  import java.io._
  import sweet.helper._
      
  def loadFrom(file: File){
    val props = SProperties.loadFromXML(file)
    for((name,v) <- props){
      logger.debug("Invoking "+name+" setter with value "+v)
      val setter = setters(name)
      setter(v)
    }
  }
  def saveTo(file: File){    
    val props = SProperties()
    for((name,getter) <- getters){
      logger.debug("Invoking "+name+" getter")
      val v = getter()
      logger.debug("Value "+v)
      props(name) = v.toString
    }
    props.storeToXML(file, "ToolboxProject Data Model", "UTF-8")
  }
}

/** Data model that represent a project for ToolboxCommand */
class ToolboxProject extends FilePropsModel{
  var name: String = _
  var templatePath: String = _
  var packageName: String = _
  var groupId: String = _
  var artifactId: String = _
  var version: String = _
  var scalaSrcPath: String = _
  var viewPath: String = _
  var controllerClassPath: String = _
  var sweetConfigClassName: String = _
  var sweetConfigClassPath: String = _
}

